```plaintext
=======================================================================
WELCOME BACK TO REGULAR EXPRESSIONS WITH PYTHON'S RE MODULE: LESSON 5
=======================================================================

Welcome back, regex adventurer! In Lesson 5, we're going to explore advanced topics like greedy vs lazy quantifiers, non-capturing groups, and multiline and dotall modes. These concepts will expand your regex toolkit even further.

As always, start by opening ipython and importing the `re` module:

```python
import re
```

=======================================================================
CONCEPT 1: GREEDY VS LAZY QUANTIFIERS
=======================================================================

By default, quantifiers in regex are greedy; they match as much text as possible. Adding a `?` after them makes them lazy, matching as little text as possible.

**Example:** Let's compare greedy and lazy matching.

```python
greedy = re.search(r'<.*>', '<div>content</div>')
lazy = re.search(r'<.*?>', '<div>content</div>')
```

`greedy` will match '<div>content</div>', while `lazy` will only match '<div>'.

=======================================================================
EXERCISE 1:
=======================================================================

Use lazy quantifiers to extract just the first HTML tag in '<p>paragraph</p><a>anchor</a>'.

```python
# Your code here
```

**Expected Outcome:** Match only the first tag, '<p>'.

=======================================================================
CONCEPT 2: NON-CAPTURING GROUPS
=======================================================================

Non-capturing groups allow you to group parts of a regex without capturing. They use the syntax `(?:...)`.

**Example:** Group without capturing in a repeated pattern.

```python
match = re.search(r'(?:ab)+', 'ababab')
```

This will match the entire 'ababab', but you can't access parts individually like captured groups.

=======================================================================
EXERCISE 2:
=======================================================================

Create a non-capturing group to match repeated 'cat' or 'dog' in 'catdogcat'.

```python
# Your code here
```

**Expected Outcome:** Match 'catdogcat' without capturing each 'cat' or 'dog'.

=======================================================================
CONCEPT 3: THE DOTALL MODE
=======================================================================

By default, the dot `.` matches any character except newlines. Using the DOTALL mode (passed as `re.DOTALL`), it can match newlines too.

**Example:** Make the dot match across lines.

```python
match = re.search(r'first.*last', 'first line\nsecond line\nlast line', re.DOTALL)
```

Try this out to see that it matches from 'first' to 'last' across multiple lines.

=======================================================================
EXERCISE 3:
=======================================================================

Use DOTALL to match all text between 'start' and 'end' in the multi-line string:

```plaintext
"""
start
line 1
line 2
end
"""
```

```python
# Your code here
```

**Expected Outcome:** Capture all lines between 'start' and 'end', including the newlines.

=======================================================================
CONCEPT 4: MULTILINE MODE
=======================================================================

The MULTILINE mode (passed as `re.MULTILINE`) treats each line in a string as a separate string for the purposes of `^` and `$`.

**Example:** Match lines that start with 'important'.

```python
matches = re.findall(r'^important', 'important line 1\nunimportant line\nimportant line 2', re.MULTILINE)
```

Run the example to see all lines starting with 'important'.

=======================================================================
EXERCISE 4:
=======================================================================

Find all lines ending with a period in the following string, using MULTILINE:

```plaintext
"""
This is the first line.
This is another line
Here is the last line.
"""
```

```python
# Your code here
```

**Expected Outcome:** Capture lines ending with a period, returning them as a list.

=======================================================================
CHALLENGE:
=======================================================================

Use what you've learned to extract all Python function definitions (`def` statements) from a script string that may span multiple lines.

```python
# Your code here
```

**Success Criteria:** Your regex should identify each complete function definition, even if they span multiple lines.

=======================================================================
FURTHER EXPLORATION:
=======================================================================

- Experiment with using `re.IGNORECASE` to make patterns case-insensitive.
- Use `re.split()` to divide strings based on regex.
- Play with backreferences in your regex patterns.
- Explore `re.compile()` to create reusable regex objects with flags.

You've now tackled some of the more advanced features of regex with Python's `re` module. These concepts open up powerful ways to manipulate and analyze text. Keep practicing and experimenting with regex in your projects. See you in the next lesson!

=======================================================================
```